const fromCharCode = String.fromCharCode
const doPadding = '' // '='
// '~hyM8J0TQIjw7ekdcqbAZ9miCgRNtsxXY64LGEaoBp5rKzHf1WnO3uP2FvVSDUl!' => b64chars
const b64mask = [95, 104, 121, 77, 56, 74, 48, 84, 81, 73, 106, 119, 55, 101, 107, 100, 99, 113, 98, 65, 90, 57, 109, 105, 67, 103, 82, 78, 116, 115, 120, 88, 89, 54, 52, 76, 71, 69, 97, 111, 66, 112, 53, 114, 75, 122, 72, 102, 49, 87, 110, 79, 51, 117, 80, 50, 70, 118, 86, 83, 68, 85, 108, 126]
let b64chars = ''
const b64tab: Record<string, number> = {}
for(let i = 0, l = b64mask.length; i < l; i++) {
  const c = fromCharCode(b64mask[i])
  b64chars += c
  b64tab[c] = i
}

const cb_utob = function(c:string) {
  if(c.length < 2) {
    const cc = c.charCodeAt(0)
    return cc < 0x80 ? c
      : cc < 0x800 ? (fromCharCode(0xc0 | (cc >>> 6)) + fromCharCode(0x80 | (cc & 0x3f)))
        : (fromCharCode(0xe0 | ((cc >>> 12) & 0x0f)) +
          fromCharCode(0x80 | ((cc >>> 6) & 0x3f)) +
          fromCharCode(0x80 | (cc & 0x3f)))
  } else {
    const cc = 0x10000 + (c.charCodeAt(0) - 0xD800) * 0x400 + (c.charCodeAt(1) - 0xDC00)
    return (fromCharCode(0xf0 | ((cc >>> 18) & 0x07)) +
      fromCharCode(0x80 | ((cc >>> 12) & 0x3f)) +
      fromCharCode(0x80 | ((cc >>> 6) & 0x3f)) +
      fromCharCode(0x80 | (cc & 0x3f)))
  }
}

const cb_encode = function(c:string) {
  const padlen = [0, 2, 1][c.length % 3]
  const ord = c.charCodeAt(0) << 16 |
    ((c.length > 1 ? c.charCodeAt(1) : 0) << 8) |
    ((c.length > 2 ? c.charCodeAt(2) : 0))
  const chars = [
    b64chars.charAt(ord >>> 18),
    b64chars.charAt((ord >>> 12) & 63),
    padlen >= 2 ? doPadding : b64chars.charAt((ord >>> 6) & 63),
    padlen >= 1 ? doPadding : b64chars.charAt(ord & 63)
  ]
  return chars.join('')
}

// decoder stuff
const re_btou = new RegExp([
  '[\xC0-\xDF][\x80-\xBF]',
  '[\xE0-\xEF][\x80-\xBF]{2}',
  '[\xF0-\xF7][\x80-\xBF]{3}'
].join('|'), 'g')

const cb_btou = function(c:string) {
  switch(c.length) {
    case 4: {
      const cp = ((0x07 & c.charCodeAt(0)) << 18) |
        ((0x3f & c.charCodeAt(1)) << 12) |
        ((0x3f & c.charCodeAt(2)) << 6) |
        (0x3f & c.charCodeAt(3))
      const offset = cp - 0x10000
      return (fromCharCode((offset >>> 10) + 0xD800) + fromCharCode((offset & 0x3FF) + 0xDC00))
    }
    case 3:
      return fromCharCode(
        ((0x0f & c.charCodeAt(0)) << 12) |
        ((0x3f & c.charCodeAt(1)) << 6) |
        (0x3f & c.charCodeAt(2))
      )
    default:
      return fromCharCode(
        ((0x1f & c.charCodeAt(0)) << 6) | (0x3f & c.charCodeAt(1))
      )
  }
}

const cb_decode = function(cccc:string) {
  const len = cccc.length
  const padLen = len % 4
  const n = (len > 0 ? b64tab[cccc.charAt(0)] << 18 : 0) |
    (len > 1 ? b64tab[cccc.charAt(1)] << 12 : 0) |
    (len > 2 ? b64tab[cccc.charAt(2)] << 6 : 0) |
    (len > 3 ? b64tab[cccc.charAt(3)] : 0)
  const chars = [
    fromCharCode(n >>> 16),
    fromCharCode((n >>> 8) & 0xff),
    fromCharCode(n & 0xff)
  ]
  chars.length -= [0, 0, 2, 1][padLen]
  return chars.join('')
}

const re_utob = /[\uD800-\uDBFF][\uDC00-\uDFFFF]|[^\x00-\x7F]/g

export function my64Encode(a:string) {
  try {
    return String(a)
      .replace(re_utob, cb_utob)
      .replace(/[\s\S]{1,3}/g, cb_encode)
  } catch(e) {
    return null
  }
}

export function my64Decode(a:string) {
  try {
    const e = String(a)
      .replace(/\?/g, '_') // 兼容性处理，稳定后可以删去
      .replace(/[^A-Za-z\d_~]/g, '')
      .replace(/[\s\S]{1,4}/g, cb_decode)
      .replace(re_btou, cb_btou)
    return e
  } catch(e) {
    return null
  }
}
